How Qutebrowser Command Completion works
像 Vim 一样,当用户输入 :
后,qutebrowser 也会弹出一个命令提示视图(CompletionView),供用户交互式输入命令。如下图所示:
这一切背后的工作机制是怎样的呢?本文将分析这背后的工作原理。
Command KeyMode
如果你不了解 Qutebrowser KeyMode 的工作原理,建议先阅读:How Qutebrowser Modes works。
Command KeyMode 的声明如下(modeman.py
):
usertypes.KeyMode.command:
modeparsers.CommandKeyParser(
mode=usertypes.KeyMode.command,
win_id=win_id,
commandrunner=commandrunner,
parent=modeman,
passthrough=True,
do_log=log_sensitive_keys,
supports_count=False),
BaseKeyParser 的构造函数加载 command 模式下的按键映射。我从中挑选了一部分:
command:
<Ctrl-P>: command-history-prev
<Ctrl-N>: command-history-next
<Up>: completion-item-focus --history prev
<Down>: completion-item-focus --history next
<Return>: command-accept
<Ctrl-Return>: command-accept --rapid
<Ctrl-B>: rl-backward-char
<Ctrl-F>: rl-forward-char
<Ctrl-Y>: rl-yank
<Escape>: mode-leave
# ...
响应冒号输入
要触发 command 模式,首先需要输入 :
,需要注意,这时我们还处在 normal
模式。从 normal
模式的按键映射表中可以看到:
normal:
<Escape>: clear-keychain ;; search ;; fullscreen --leave
o: cmd-set-text -s :open
# ...
/: cmd-set-text /
?: cmd-set-text ?
":": "cmd-set-text :"
输入 :
将触发 cmd-set-text
命令。
cmd-set-text
命令
位于 mainwindow/statusbar/command.py
:
@cmdutils.register(instance='status-command', name='cmd-set-text',
scope='window', maxsplit=0, deprecated_name='set-cmd-text')
@cmdutils.argument('count', value=cmdutils.Value.count)
def cmd_set_text_command(self, text: str,
count: int = None,
space: bool = False,
append: bool = False,
run_on_count: bool = False) -> None:
"""Preset the statusbar to some text.
...
"""
#...
# note: STARTCHARS = ":/?"
if not text or text[0] not in modeparsers.STARTCHARS:
raise cmdutils.CommandError(
"Invalid command text '{}'.".format(text))
if run_on_count and count is not None:
# ...
else:
self.cmd_set_text(text)
def cmd_set_text(self, text: str) -> None:
"""Preset the statusbar to some text.
"""
# update the text from StatusBar's Command view
self.setText(text)
log.modes.debug("Setting command text, focusing {!r}".format(self))
# go into command mode
modeman.enter(self._win_id, usertypes.KeyMode.command, 'cmd focus')
self.setFocus()
# show command signal
self.show_cmd.emit()
在上面代码中:
STARTCHARS
常量中包含":/?"
,首先验证输入字符是否是STARTCHARS
其中之一- 在 cmd_set_text 中:
- 首先更新 StatusBar 内部 Command 视图的文本
- 之后,获取当前活跃的 ModeManager,进入命令模式
- 最后发出
show_cmd
信号
show_cmd signal
在 StatusBar 的构造函数,注册了 show_cmd 信号的监听:
self.cmd.show_cmd.connect(self._show_cmd_widget)
self.cmd.hide_cmd.connect(self._hide_cmd_widget)
_show_cmd_widget
:
def _show_cmd_widget(self):
"""Show command widget instead of temporary text."""
self._stack.setCurrentWidget(self.cmd)
self.show()
self.cmd
的类型是 Command
,他是 CommandLineEdit
的子类,是一个单行输入框。
Command 输入框
现在 Command 输入框已经在 Statusbar 中展示出来,并带有初始文本 :
。接下来,用户在其中输入命令,这会触发:
self.cursorPositionChanged.connect(self.update_completion)
self.textChanged.connect(self.update_completion)
self.textChanged.connect(self.updateGeometry)
self.textChanged.connect(self._incremental_search)
update_completion
是一个信号,接收方位于 Completer 类中,从类名中可以看出,Completer 专门负责命令补全。关于 update_completion
的响应将在下节中介绍。
我们先看 _incremental_search
:
@pyqtSlot()
def _incremental_search(self) -> None:
if not config.val.search.incremental:
return
self._handle_search()
def _handle_search(self) -> bool:
"""Check if the currently entered text is a search, and if so, run it.
Return:
True if a search was executed, False otherwise.
"""
if self.prefix() == '/':
self.got_search.emit(self.text()[1:], False)
return True
elif self.prefix() == '?':
self.got_search.emit(self.text()[1:], True)
return True
else:
return False
从中可以看出,如果是搜索命令(以 /
或 ?
)为开头,则触发 got_search
信号。
Completer.update_completion
在 Completer 的构造函数中,注册了 Command 的 update_completion
信号:
self._cmd.update_completion.connect(self.schedule_completion_update)
schedule_completion_update
:
@pyqtSlot()
def schedule_completion_update(self):
"""Schedule updating/enabling completion.
"""
_cmd, _sep, rest = self._cmd.text().partition(' ')
input_length = len(rest)
if (0 < input_length < config.val.completion.min_chars and
self._cmd.cursorPosition() > self._last_cursor_pos):
log.completion.debug("Ignoring update because the length of "
"the text is less than completion.min_chars.")
elif (self._cmd.cursorPosition() == self._last_cursor_pos and
self._cmd.text() == self._last_text):
log.completion.debug("Ignoring update because there were no "
"changes.")
else:
log.completion.debug("Scheduling completion update.")
start_delay = config.val.completion.delay if self._last_text else 0
self._timer.start(start_delay)
self._last_cursor_pos = self._cmd.cursorPosition()
self._last_text = self._cmd.text()
如果内容发生改变,并触发开始补全的阈值,则通过定时器 _timer
异步触发补全。
_timer
关联的槽函数是 _update_completion
:
@pyqtSlot()
def _update_completion(self):
"""Check if completions are available and activate them."""
# get CompletionView
completion = self._completion()
# ...
before_cursor, pattern, after_cursor = self._partition()
# ...
pattern = pattern.strip("'\"")
# Get the completion function based on the current command text.
func = self._get_new_completion(before_cursor, pattern)
# ...
self._last_before_cursor = before_cursor
args = (x for x in before_cursor[1:] if not x.startswith('-'))
cur_tab = objreg.get('tab', scope='tab', window=self._win_id,
tab='current')
with debug.log_time(log.completion, 'Starting {} completion'
.format(func.__name__)):
info = CompletionInfo(config=config.instance,
keyconf=config.key_instance,
win_id=self._win_id,
cur_tab=cur_tab)
model = func(*args, info=info)
with debug.log_time(log.completion, 'Set completion model'):
completion.set_model(model)
completion.set_pattern(pattern)
本文作者:Maeiee
本文链接:How Qutebrowser Command Completion works
版权声明:如无特别声明,本文即为原创文章,版权归 Maeiee 所有,未经允许不得转载!
喜欢我文章的朋友请随缘打赏,鼓励我创作更多更好的作品!